Introduction & Learning Goals
This expanded module gives you mastery over arrays, functions, and modular architecture in JavaScript. We'll move from fundamentals to advanced patterns used in production: higher-order functions, closures, immutability, pure functions, performance considerations, and module design. The practical outcome is a robust TODO application structured into modules with tests, persistent storage, and clear separation of concerns.
By the end of this lesson you will be able to:
- Explain closures, scopes and how functions capture variables.
- Use array methods (map, filter, reduce, find, some, every) expertly and choose the right one for the job.
- Write higher-order functions and compose small pure functions.
- Organize code into ES modules and reason about dependencies and side-effects.
- Improve performance for large arrays and DOM updates using best practices.
- Build, test and refactor a modular TODO app with persistence.
Key Terms & Definitions
- Closure
- A function together with the lexical environment in which it was declared — it "remembers" outer variables.
- Higher-order function
- A function that accepts other functions as arguments or returns a function.
- Pure function
- Function that returns a value based only on its inputs and has no side effects.
- Immutability
- Pattern of not changing data in place; instead return new copies when transformations are needed.
- Map/Filter/Reduce
- Core array methods for transforming, selecting and aggregating data.
- Event delegation
- Attach a single event listener to a parent to handle events from many children efficiently.
- Module
- Self-contained file or unit exposing an API via exports and hiding internal details.
- Pure side-effect
- Operations like network calls or DOM mutations — keep these at the edges of your app.
- Memoization
- Caching function results to avoid recomputation.
- Debounce
- Delay function execution until a pause in events.
Theory — Functions, Arrays & Patterns
Closures and Scope
Closures are a foundational concept. A function defined inside another function has access to the outer function's variables even after the outer function returns. This is powerful for data encapsulation and factories.
// Counter factory using closure
function createCounter(initial = 0){
let count = initial;
return {
increment(){ count++; return count; },
decrement(){ count--; return count; },
value(){ return count; }
};
}
const c = createCounter(5);
console.log(c.increment()); // 6
Note how the internal count is not directly accessible and only mutated via the returned methods. This pattern avoids global state and accidental mutation.
Higher-order functions and composition
Functions that accept other functions promote reusability. Compose small functions to create more complex behavior:
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const add1 = x => x + 1;
const double = x => x * 2;
const result = pipe(add1, double)(3); // (3+1)*2 = 8
Array methods — map, filter, reduce
Use map to transform arrays, filter to select items, and reduce for aggregation:
const nums = [1,2,3,4];
const squares = nums.map(n => n*n); // [1,4,9,16]
const evens = nums.filter(n => n % 2 === 0); // [2,4]
const sum = nums.reduce((acc,n) => acc + n, 0); // 10
Avoid side-effects inside these methods; prefer returning new values.
Immutability & Pure Functions
Immutability reduces bugs. Instead of mutating arrays with push or splice, use spread and slice to return new arrays.
const arr = [1,2,3];
const arr2 = [...arr, 4]; // [1,2,3,4]
const removed = arr2.filter(x => x !== 2); // [1,3,4]
Event delegation & efficient DOM updates
Instead of adding listeners to many elements, attach a listener to a parent and use event.target to determine the source. For frequent updates, batch DOM changes using DocumentFragment or re-render ranges only instead of full replace.
Modules & organizing code
Split your app into modules: storage.js (persistence), ui.js (rendering), app.js (controller). Use ES modules (export/import) to keep code testable. Keep side-effects at the top-level controller to make modules pure where possible.
Practical Project — Modular TODO App (Overview)
We will build a TODO app split into three modules in a single HTML playground for offline use:
- storage: handles saving to localStorage and loading.
- model: pure functions for manipulating todo arrays (add, remove, toggle, filter).
- ui: renders list and handles event delegation.
This separation makes unit testing straightforward and reduces bugs.
Playground — TODO App Starter
Edit and run the app in the preview. The starter is modular and written inline for simplicity; in production split into separate files and import as modules.
Practical Labs — Progressive Steps
Lab 1 — Refactor model functions
- Write unit tests (simple functions) to assert add/remove/toggle behaviors using plain assertions or a lightweight test helper.
- Ensure functions are pure: no side-effects and always return new arrays.
Lab 2 — Improve performance for large lists
- When dealing with thousands of items, avoid re-rendering the entire list. Only update the item changed or use virtualization techniques.
- Use DocumentFragment as shown, and avoid innerHTML for large updates.
Lab 3 — Add filtering and derived views
- Add buttons for All, Active, Completed. Implement model.filterViews(todos, view) pure function returning filtered lists.
- Cache derived results where appropriate and invalidate cache on mutations.
Lab 4 — Modular testing and CI integration
- Extract pure logic into a file
model.test.jsand create simple test runner or use a framework when available. - Integrate a basic GitHub Action to run tests on push to main.
Advanced Techniques
Memoization
Memoize heavy computations to avoid repeated work. Simple example:
function memoize(fn){
const cache = new Map();
return function(arg){
if(cache.has(arg)) return cache.get(arg);
const res = fn(arg); cache.set(arg, res); return res;
};
}
Batching updates & Debounce
Debounce user input to prevent frequent model operations; batch DOM updates to limit reflows.
function debounce(fn, wait=300){
let t; return (...args) => { clearTimeout(t); t = setTimeout(()=> fn(...args), wait); };
}
Error handling and validation
Validate inputs at the boundaries (UI) and in the model. Use try/catch around operations that could throw and provide clear user messages.
"Two are better than one…" — Ecclesiastes 4:9. Collaborate, review each other's code, and mentor juniors — teamwork multiplies results.
Knowledge Check — 20 Questions
- Q1. What is a closure?
- Q2. Which method transforms every element in an array?
- Q3. What should a pure function avoid?
- Q4. Which array method reduces array to single value?
- Q5. What does event delegation help with?
- Q6. How to create a new array with an added item without mutating?
- Q7. Which is a higher-order function?
- Q8. What is memoization used for?
- Q9. Which is recommended for large lists?
- Q10. What is the advantage of pure functions?
- Q11. Which creates a shallow copy of an object?
- Q12. Debounce helps to:
- Q13. Which method finds first matching element?
- Q14. Where should side-effects be kept?
- Q15. Which is true about ES modules?
- Q16. How to toggle done status immutably?
- Q17. What does DocumentFragment help with?
- Q18. Which is true about reduce?
- Q19. Good test for model functions includes:
- Q20. Which helps avoid accidental mutation in arrays?